/*
* Sun Public License Notice
*
* The contents of this file are subject to the Sun Public License
* Version 1.0 (the "License"). You may not use this file except in
* compliance with the License. A copy of the License is available at
* http://www.sun.com/
*
* The Original Code is NetBeans. The Initial Developer of the Original
* Code is Sun Microsystems, Inc. Portions Copyright 1997-2000 Sun
* Microsystems, Inc. All Rights Reserved.
*/
package org.netbeans.modules.properties;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeEvent;
import java.io.*;
import java.lang.reflect.*;
import java.util.Iterator;
import java.text.MessageFormat;
import javax.swing.event.DocumentListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.ChangeEvent;
import javax.swing.Timer;
import javax.swing.text.Document;
import javax.swing.text.StyledDocument;
import javax.swing.text.EditorKit;
import javax.swing.text.BadLocationException;
import org.openide.util.WeakListener;
import org.openide.util.NbBundle;
import org.openide.util.Task;
import org.openide.util.TaskListener;
import org.openide.util.RequestProcessor;
import org.openide.text.EditorSupport;
import org.openide.text.PositionRef;
import org.openide.cookies.EditCookie;
import org.openide.cookies.SaveCookie;
import org.openide.cookies.OpenCookie;
import org.openide.loaders.MultiDataObject;
import org.openide.loaders.DataObject;
import org.openide.filesystems.FileObject;
import org.openide.windows.CloneableTopComponent;
import org.openide.windows.TopComponent;
import org.openide.nodes.NodeAdapter;
import org.openide.nodes.Node;
import org.openide.TopManager;
import org.openide.NotifyDescriptor;
/** Support for viewing porperties files (EditCookie) by opening them in a text editor */
public class PropertiesEditorSupport extends EditorSupport implements EditCookie, Serializable {
/** Timer which countdowns the auto-reparsing time. */
javax.swing.Timer timer;
/** New lines in this file was delimited by '\n' */
static final byte NEW_LINE_N = 0;
/** New lines in this file was delimited by '\r' */
static final byte NEW_LINE_R = 1;
/** New lines in this file was delimited by '\r\n' */
static final byte NEW_LINE_RN = 2;
/** The type of new lines */
byte newLineType = NEW_LINE_N;
/** The flag saying if we should listen to the document modifications */
private boolean listenToEntryModifs = true;
private Document listenDocument;
/** Listener to the document changes - entry. The superclass holds a saving manager
* for the whole dataobject. */
private EntrySavingManager entryModifL;
/** Properties Settings */
static final PropertiesSettings settings = new PropertiesSettings();
static final long serialVersionUID =1787354011149868490L;
/** Constructor */
public PropertiesEditorSupport(PropertiesFileEntry entry) {
super (entry);
//System.out.println("editor support constructor - " + entry.getFile().getName());
//Thread.dumpStack();
initialize();
}
public void initialize() {
myEntry = (PropertiesFileEntry)entry;
super.setModificationListening(false);
setMIMEType (PropertiesDataObject.MIME_PROPERTIES);
initTimer();
// listen to myself so I can add a listener for changes when the document is loaded
addChangeListener(new ChangeListener() {
public void stateChanged(ChangeEvent evt) {
if (isDocumentLoaded()) {
setListening(true);
}
}
});
//PENDING
// set actions
/*setActions (new SystemAction [] {
SystemAction.get (CutAction.class),
SystemAction.get (CopyAction.class),
SystemAction.get (PasteAction.class),
});*/
}
void setRef(CloneableTopComponent.Ref ref) {
allEditors = ref;
}
Object writeReplace() throws ObjectStreamException {
return new SerialProxy(myEntry);
}
public static class SerialProxy implements Serializable {
static final long serialVersionUID =2675098551717845346L;
public SerialProxy(PropertiesFileEntry serialEntry) {
this.serialEntry = serialEntry;
}
private PropertiesFileEntry serialEntry;
Object readResolve() throws ObjectStreamException {
//System.out.println("deserializing properties editor");
//System.out.println("serialEntry " + serialEntry);
//System.out.println("dataobject " + serialEntry.getDataObject());
//Thread.dumpStack();
Object pe = serialEntry.getPropertiesEditor();
//System.out.println("deserializing properties editor END");
return pe;
}
}
/** Visible view of underlying file entry */
transient PropertiesFileEntry myEntry;
/** Focuses existing component to open, or if none exists creates new.
* @see OpenCookie#open
*/
public void open () {
CloneableTopComponent editor = openCloneableTopComponent2();
editor.requestFocus();
}
/** Simply open for an editor. */
protected final CloneableTopComponent openCloneableTopComponent2() {
MessageFormat mf = new MessageFormat (NbBundle.getBundle(PropertiesEditorSupport.class).
getString ("CTL_PropertiesOpen"));
synchronized (allEditors) {
try {
CloneableTopComponent ret = (CloneableTopComponent)allEditors.getAnyComponent ();
ret.open();
return ret;
} catch (java.util.NoSuchElementException ex) {
// no opened editor
TopManager.getDefault ().setStatusText (mf.format (
new Object[] {entry.getFile().getName()}));
CloneableTopComponent editor = createCloneableTopComponent ();
allEditors = editor.getReference ();
editor.open();
TopManager.getDefault ().setStatusText (NbBundle.getBundle(DataObject.class).getString ("CTL_ObjectOpened"));
return editor;
}
}
}
/** Launches the timer for autoreparse */
private void initTimer() {
// initialize timer
timer = new Timer(0, new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent e) {
myEntry.getHandler().autoParse();
}
});
timer.setInitialDelay(settings.getAutoParsingDelay());
timer.setRepeats(false);
}
/** Returns whether there is an open component (editor or open). */
public synchronized boolean hasOpenComponent() {
return (hasOpenTableComponent() || hasOpenEditorComponent());
}
private synchronized boolean hasOpenTableComponent() {
//System.out.println("hasOpenComponent (table) " + myEntry.getFile().getPackageNameExt('/','.') + " " + ((PropertiesDataObject)myEntry.getDataObject()).getOpenSupport().hasOpenComponent());
return ((PropertiesDataObject)myEntry.getDataObject()).getOpenSupport().hasOpenComponent();
}
/** Returns whether there is an open editor component. */
public synchronized boolean hasOpenEditorComponent() {
java.util.Enumeration en = allEditors.getComponents ();
//System.out.println("hasOpenComponent (editor) " + myEntry.getFile().getPackageNameExt('/','.') + " " + en.hasMoreElements ());
return en.hasMoreElements ();
}
public void saveThisEntry() throws IOException {
super.saveDocument();
myEntry.setModified(false);
}
public boolean close() {
SaveCookie savec = (SaveCookie) myEntry.getCookie(SaveCookie.class);
if ((savec != null) && hasOpenTableComponent()) {
return false;
}
//System.out.println("closing");
if (!super.close())
return false;
//System.out.println("closed - document open = " + isDocumentLoaded());
closeDocumentEntry();
myEntry.getHandler().reparseNowBlocking();
return true;
}
/** Clears all data from memory.
*/
/* protected void closeDocument () {
super.closeDocument();
closeDocumentEntry();
}*/
/** Utility method which enables or disables listening to modifications
* on asociated document.
* <P>
* Could be useful if we have to modify document, but do not want the
* Save and Save All actions to be enabled/disabled automatically.
* Initially modifications are listened to.
* @param listenToModifs whether to listen to modifications
*/
public void setModificationListening (final boolean listenToModifs) {
//System.out.println("set modification listening - " + listenToModifs);
this.listenToEntryModifs = listenToModifs;
if (getDocument() == null) return;
setListening(listenToEntryModifs);
}
/* A method to create a new component. Overridden in subclasses.
* @return the {@link Editor} for this support
*/
protected CloneableTopComponent createCloneableTopComponent () {
// initializes the document if not initialized
prepareDocument ();
DataObject obj = myEntry.getDataObject ();
Editor editor = new PropertiesEditor (obj, this);
return editor;
}
/** Should test whether all data is saved, and if not, prompt the user
* to save. Called by my topcomponent when it wants to close its last topcomponent, but the table editor may still be open
*
* @return <code>true</code> if everything can be closed
*/
protected boolean canClose () {
SaveCookie savec = (SaveCookie) myEntry.getCookie(SaveCookie.class);
if (savec != null) {
// if the table is open, can close without worries, don't remove the save cookie
if (hasOpenTableComponent())
return true;
// PENDING - is not thread safe
MessageFormat format = new MessageFormat(NbBundle.getBundle(PropertiesEditorSupport.class).
getString("MSG_SaveFile"));
String msg = format.format(new Object[] { entry.getFile().getName()});
NotifyDescriptor nd = new NotifyDescriptor.Confirmation(msg, NotifyDescriptor.YES_NO_CANCEL_OPTION);
Object ret = TopManager.getDefault().notify(nd);
// cancel
if (NotifyDescriptor.CANCEL_OPTION.equals(ret))
return false;
// yes
if (NotifyDescriptor.YES_OPTION.equals(ret)) {
try {
savec.save();
}
catch (IOException e) {
TopManager.getDefault().notifyException(e);
return false;
}
}
// no
if (NotifyDescriptor.NO_OPTION.equals(ret)) {
return true;
}
}
return true;
}
/** Read the file from the stream, filter the guarded section
* comments, and mark the sections in the editor.
*
* @param doc the document to read into
* @param stream the open stream to read from
* @param kit the associated editor kit
* @throws IOException if there was a problem reading the file
* @throws BadLocationException should not normally be thrown
* @see #saveFromKitToStream
*/
protected void loadFromStreamToKit (StyledDocument doc, InputStream stream, EditorKit kit) throws IOException, BadLocationException {
NewLineInputStream is = new NewLineInputStream(stream);
try {
kit.read(is, doc, 0);
newLineType = is.getNewLineType();
}
finally {
is.close();
}
}
/** Store the document and add the special comments signifying
* guarded sections.
*
* @param doc the document to write from
* @param kit the associated editor kit
* @param stream the open stream to write to
* @throws IOException if there was a problem writing the file
* @throws BadLocationException should not normally be thrown
* @see #loadFromStreamToKit
*/
protected void saveFromKitToStream(StyledDocument doc, EditorKit kit, OutputStream stream) throws IOException, BadLocationException {
//System.out.println("saving - doc = " + doc);
OutputStream os = new NewLineOutputStream(stream, newLineType);
try {
kit.write(os, doc, 0, doc.getLength());
}
finally {
if (os != null) {
try {
os.close();
}
catch (IOException e) {
}
}
}
}
/** Does part of the cleanup - removes a listener.
*/
private void closeDocumentEntry () {
// listen to modifs
if (listenToEntryModifs) {
getEntryModifL().clearSaveCookie();
setListening(false);
}
}
private void setListening(boolean listen) {
if (listen) {
if ((getDocument() == null) || (listenDocument == getDocument()))
return;
if (listenDocument != null) // also holds that listenDocument != getDocument()
listenDocument.removeDocumentListener(getEntryModifL());
listenDocument = getDocument();
listenDocument.addDocumentListener(getEntryModifL());
}
else {
if (listenDocument != null) {
listenDocument.removeDocumentListener(getEntryModifL());
listenDocument = null;
}
}
}
/** Visible view of the underlying method. */
public Editor openAt(PositionRef pos) {
return super.openAt(pos);
}
/** Returns a EditCookie for editing at a given position. */
public PropertiesEditAt getViewerAt(String key) {
return new PropertiesEditAt (key);
}
/** Class for opening at a given key. */
public class PropertiesEditAt implements EditCookie {
private String key;
PropertiesEditAt(String key) {
this.key = key;
}
public void setKey(String key) {
this.key = key;
}
public String getKey() {
return key;
}
public void edit() {
Element.ItemElem item = myEntry.getHandler().getStructure().getItem(key);
if (item != null) {
PositionRef pos = item.getKeyElem().getBounds().getBegin();
PropertiesEditorSupport.this.openAt(pos);
}
else {
PropertiesEditorSupport.this.edit();
}
}
}
/** Returns an entry saving manager. */
private synchronized EntrySavingManager getEntryModifL () {
if (entryModifL == null) {
entryModifL = new EntrySavingManager();
// listens whether to add or remove SaveCookie
myEntry.addPropertyChangeListener(entryModifL);
}
return entryModifL;
}
/** Make modifiedApendix accessible for inner classes. */
String getModifiedAppendix() {
return modifiedAppendix;
}
/** Cloneable top component to hold the editor kit.
*/
public static class PropertiesEditor extends EditorSupport.Editor {
/** Holds the file being edited */
protected transient PropertiesFileEntry entry;
private transient PropertiesEditorSupport propSupport;
/** Listener for entry's save cookie changes */
private transient PropertyChangeListener saveCookieLNode;
/** Listener for entry's name changes */
private transient NodeAdapter nodeL;
static final long serialVersionUID =-2702087884943509637L;
/** Constructor for deserialization */
public PropertiesEditor() {
super();
}
/** Creates new editor */
public PropertiesEditor(DataObject obj, PropertiesEditorSupport support) {
super(obj, support);
this.propSupport = support;
initMe();
}
/** initialization after construction and deserialization */
private void initMe() {
this.entry = propSupport.myEntry;
// add to EditorSupport - patch for a bug in deserialization
propSupport.setRef(getReference());
entry.getNodeDelegate().addNodeListener (
new WeakListener.Node(nodeL =
new NodeAdapter () {
public void propertyChange (PropertyChangeEvent ev) {
if (ev.getPropertyName ().equals (Node.PROP_DISPLAY_NAME)) {
updateName();
}
}
}
));
Node n = entry.getNodeDelegate ();
setActivatedNodes (new Node[] { n });
updateName();
// entry to the set of listeners
saveCookieLNode = new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent evt) {
if (PresentableFileEntry.PROP_COOKIE.equals(evt.getPropertyName()) ||
PresentableFileEntry.PROP_NAME.equals(evt.getPropertyName())) {
updateName();
}
}
};
this.entry.addPropertyChangeListener(
new WeakListener.PropertyChange(saveCookieLNode));
}
/** When closing last view, also close the document.
* @return <code>true</code> if close succeeded
*/
protected boolean closeLast () {
// instead of super
if (!propSupport.canClose ()) {
// if we cannot close the last window
return false;
}
boolean doCloseDoc = !propSupport.hasOpenTableComponent();
//SaveCookie savec = (SaveCookie) entry.getCookie(SaveCookie.class);
try {
if (doCloseDoc) {
// propSupport.closeDocument (); by reflection
Method closeDoc = EditorSupport.class.getDeclaredMethod("closeDocument", new Class[0]);
closeDoc.setAccessible(true);
closeDoc.invoke(propSupport, new Object[0]);
}
/* if (propSupport.lastSelected == this) {
propSupport.lastSelected = null; by reflection */
Field lastSel = EditorSupport.class.getDeclaredField("lastSelected");
lastSel.setAccessible(true);
if (lastSel.get(propSupport) == this)
lastSel.set(propSupport, null);
}
catch (Exception e) {
if (Boolean.getBoolean("netbeans.debug.exceptions"))
e.printStackTrace();
}
// end super
/*boolean canClose = super.closeLast();
if (!canClose)
return false;*/
if (doCloseDoc) {
propSupport.closeDocumentEntry();
entry.getHandler().reparseNowBlocking();
}
return true;
}
/** Updates the name of this top component according to
* the existence of the save cookie in ascoiated data object
*/
protected void updateName () {
if (entry == null) {
setName("");
return;
}
else {
String name = entry.getFile().getName();
if (entry.getCookie(SaveCookie.class) != null)
setName(name + propSupport.getModifiedAppendix());
else
setName(name);
}
}
/* Serialize this top component.
* @param out the stream to serialize to
*/
public void writeExternal (ObjectOutput out)
throws IOException {
super.writeExternal(out);
out.writeObject(propSupport);
}
/* Deserialize this top component.
* @param in the stream to deserialize from
*/
public void readExternal (ObjectInput in)
throws IOException, ClassNotFoundException {
super.readExternal(in);
propSupport = (PropertiesEditorSupport)in.readObject();
initMe();
}
} // end of PropertiesEditor inner class
/** EntrySavingManager manages two tasks concerning saving:<P>
* 1) It tracks changes in document asociated with ther entry and
* sets modification flag appropriately.<P>
* 2) This class also implements functionality of SaveCookie interface
*/
private final class EntrySavingManager implements DocumentListener, SaveCookie, PropertyChangeListener {
/*********** Implementation of the DocumentListener *******/
/** Gives notification that an attribute or set of attributes changed.
* @param ev event describing the action
*/
public void changedUpdate(DocumentEvent ev) {
// do nothing - just an attribute
}
/** Gives notification that there was an insert into the document.
* @param ev event describing the action
*/
public void insertUpdate(DocumentEvent ev) {
modified();
changeStructureStatus();
}
/** Gives notification that a portion of the document has been removed.
* @param ev event describing the action
*/
public void removeUpdate(DocumentEvent ev) {
modified();
changeStructureStatus();
}
private void changeStructureStatus() {
int delay = settings.getAutoParsingDelay();
myEntry.getHandler().setDirty(true);
if (delay > 0) {
timer.setInitialDelay(delay);
timer.restart();
}
}
/** Gives notification that the DataObject was changed.
* @param ev PropertyChangeEvent
*/
public void propertyChange(PropertyChangeEvent ev) {
if ((ev.getSource() == myEntry) &&
(PropertiesFileEntry.PROP_MODIFIED.equals(ev.getPropertyName()))) {
if (((Boolean) ev.getNewValue()).booleanValue()) {
addSaveCookie();
} else {
removeSaveCookie();
}
}
}
/******* Implementation of the Save Cookie *********/
public void save () throws IOException {
// do saving job
saveThisEntry();
}
void clearSaveCookie() {
// remove save cookie (if save was succesfull)
myEntry.setModified(false);
}
/** Sets modification flag.
*/
private void modified () {
myEntry.setModified(true);
}
/** Adds save cookie to the DO. Only if a component is open, otherwise saves it right away
*/
private void addSaveCookie() {
if (myEntry.getCookie(SaveCookie.class) == null) {
myEntry.getCookieSet().add(this);
}
((PropertiesDataObject)myEntry.getDataObject()).updateModificationStatus();
if (!hasOpenComponent()) {
RequestProcessor.postRequest(new Runnable() {
public void run() {
myEntry.getPropertiesEditor().open();
}
});
}
}
/** Removes save cookie from the DO.
*/
private void removeSaveCookie() {
// remove Save cookie from the data object
if (myEntry.getCookie(SaveCookie.class) == this) {
myEntry.getCookieSet().remove(this);
}
((PropertiesDataObject)myEntry.getDataObject()).updateModificationStatus();
}
} // end of EntrySavingManager inner class
/** This stream is able to filter various new line delimiters and replace them by \n.
*/
static class NewLineInputStream extends InputStream {
/** Encapsulated input stream */
BufferedInputStream bufis;
/** Next character to read. */
int nextToRead;
/** The count of types new line delimiters used in the file */
int[] newLineTypes;
/** Creates new stream.
* @param is encapsulated input stream.
* @param justFilter The flag determining if this stream should
* store the guarded block information. True means just filter,
* false means store the information.
*/
public NewLineInputStream(InputStream is) throws IOException {
bufis = new BufferedInputStream(is);
nextToRead = bufis.read();
newLineTypes = new int[] { 0, 0, 0 };
}
/** Reads one character.
* @return next char or -1 if the end of file was reached.
* @exception IOException if any problem occured.
*/
public int read() throws IOException {
if (nextToRead == -1)
return -1;
if (nextToRead == '\r') {
nextToRead = bufis.read();
while (nextToRead == '\r')
nextToRead = bufis.read();
if (nextToRead == '\n') {
nextToRead = bufis.read();
newLineTypes[NEW_LINE_RN]++;
return '\n';
}
else {
newLineTypes[NEW_LINE_R]++;
return '\n';
}
}
if (nextToRead == '\n') {
nextToRead = bufis.read();
newLineTypes[NEW_LINE_N]++;
return '\n';
}
int oldNextToRead = nextToRead;
nextToRead = bufis.read();
return oldNextToRead;
}
public byte getNewLineType() {
if (newLineTypes[0] > newLineTypes[1]) {
return (newLineTypes[0] > newLineTypes[2]) ? (byte) 0 : 2;
}
else {
return (newLineTypes[1] > newLineTypes[2]) ? (byte) 1 : 2;
}
}
}
/** This stream is used for changing the new line delimiters.
* It replaces the '\n' by '\n', '\r' or "\r\n"
*/
static class NewLineOutputStream extends OutputStream {
/** Underlaying stream. */
OutputStream stream;
/** The type of new line delimiter */
byte newLineType;
/** Creates new stream.
* @param stream Underlaying stream
* @param newLineType The type of new line delimiter
*/
public NewLineOutputStream(OutputStream stream, byte newLineType) {
this.stream = stream;
this.newLineType = newLineType;
}
/** Write one character.
* @param b char to write.
*/
public void write(int b) throws IOException {
if (b == '\r')
return;
if (b == '\n') {
switch (newLineType) {
case NEW_LINE_R:
stream.write('\r');
break;
case NEW_LINE_RN:
stream.write('\r');
case NEW_LINE_N:
stream.write('\n');
break;
}
}
else {
stream.write(b);
}
}
/** Closes the underlaying stream.
*/
public void close() throws IOException {
stream.flush();
stream.close();
}
}
}